<?PHP
	/************************************************************************
	*       This file, in addition to all other parts of X7 Chat are        *
	*                   Copyright 2003-2007 by Tim Chamness.                *
	* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
	* Version:          X7 Chat 3.0.0-A2                                    *
	* Release Date:     October 20, 2007                                    *
	* Author:           Tim Chamness                                        *
	* Copyright:        2003-2007 by Tim Chamness                           *
	* Website:          http://www.x7chat.com/                              *
	* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
	* X7 Chat 3 is free software; you can redistribute it and/or modify     *
	* it under the terms of the GNU General Public License as published by  *
	* the Free Software Foundation; either version 3 of the License, or     *
	* (at your option) any later version.                                   *
	*                                                                       *
	* X7 Chat 3 is distributed in the hope that it will be useful, but      *
	* WITHOUT ANY WARRANTY; without even the implied warranty of            *
	* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU     *
	* General Public License for more details.                              *
	*                                                                       *
	* You should have received a copy of the GNU General Public License     *
	* along with this program.  If not, see <http://www.gnu.org/licenses/>. *
	*                                                                       *
	* Additional Terms, compatible with Section 7 of the GNU GPL v3,        *
	* apply to X7 Chat 3 and all X7 Chat 3 files, including this one.       *
	* You should have received a copy of these Additional Terms in the      *
	* file docs/COPYING.ADDITIONAL_TERMS.  If you did not receive a copy    *
	* please visit <http://www.x7chat.com/COPYING.ADDITIONAL_TERMS/>.       *
	************************************************************************/
	
	/**
	This class is the primary processor of X7 Chat update and modification
	patches.  It is based on GNU patch and processes modified unified diff
	files.
	
	This class depends on lib_ftp for file access functions.
		
	Notes: Patch files must have their contents in this order:
		(m) CHMOD commands for deleted files and directories, file must 
			already exist
		(r) Delete commands for deleting files and directories
		(c) Directory creation commands, parent directory must exist
		(f) File creation commands, parent directory must exist
		(m) CHMOD commands for newly created files and directories
		(b) Binary writes, file must exist
		diff commands, file must exist
	**/
	
	class lib_patch {
	
		var $file_data = array();
		var $fs_operations = array('c'=>array(),'f'=>array(),'r'=>array(),'b'=>array(),'m'=>array());
		var $errors = array('warning'=>array(),'fatal'=>array());
		var $base;
		var $pd;
		var $ftp;

		// This function requires the relative or absolute path to the patch_file
		function lib_patch($patch_file,$lib_ftp,$base='.'){
			$this->base = $base;
			$this->pd = file($patch_file);
			$this->ftp = $lib_ftp;
		}

		function apply_patch(){
			$lines = count($this->pd);
			$curfile = $curline = $curlines = null;
			$hunk_ok = $file_ok = true;
			
			for($i = 0; $i < $lines; $i++){
			
				// Skip remainder of this file
				if(!$file_ok && $this->pd[$i][0] != 'd')
					continue;
					
				// Skip remainder of this hunk
				if(!$hunk_ok && $this->pd[$i][0] != 'd' && $this->pd[$i][0] != '@')
					continue;

				switch($this->pd[$i][0]){
					case 'd':
						preg_match("#^diff -ur \./(.+?) \./(.+?)$#",trim($this->pd[$i]),$match);
						$i += 2;
						
						$file_ok = (count($match) == 3);
						
						if($file_ok){
							$curfile = $this->base.'/'.$match[1];
							
							if(!is_file($curfile) && !$this->willbe_file($curfile)){
								$this->errors['fatal'][] = "File `$curfile` does not exist at patch line `$i`";
							}else{
								$curline = null;
								if(!isset($this->file_data[$curfile])){
									if(is_file($curfile))
										$this->file_data[$curfile] = file($curfile);
									else
										$this->file_data[$curfile] = array();
								}
								$curlines = count($this->file_data[$curfile]);
							}
							
						}else{
							$this->errors['fatal'][] = "Malformed diff header at patch line `$i`";
						}
						
					break;
					case '@':
						preg_match("#^@@ \-([0-9,]+) \+([0-9,]+) @@$#",trim($this->pd[$i]),$match);
						
						$hunk_ok = (count($match) == 3);
							
						if($hunk_ok){
							$match[2] = explode(',',$match[2]);
							
							$hunk_ok = (count($match[2]) == 2);
							
							if($hunk_ok){
								$start = $match[2][0]-1;
								
								if($this->pd[$i+1][0] == ' ' || $this->pd[$i+1][0] == "\n"){
									$find = trim($this->pd[++$i]);
			
									for($offset = 0; $start+$offset < $curlines || $start-$offset >= 0; $offset++){
										if($start+$offset < $curlines && trim($this->file_data[$curfile][$start+$offset]) == $find){
											$curline = $start+$offset;
											break;
										}elseif($start-$offset >= 0 && trim($this->file_data[$curfile][$start-$offset]) == $find){
											$curline = $start-$offset;
											break;
										}
									}
									
									$hunk_ok = ($curline !== null);
									if(!$hunk_ok){
										$this->errors['fatal'][] = "Failed to find offset `{$find}` from patch line `$i` starting at `{$curfile}` line `$start`";
									}
									
									if($offset > 0){
										$this->errors['warning'][] = "Patching `{$curfile}` with offset `".(($curfile > $start) ? $offset : '-'.$offset)."` at patch line `$i`";
									}
								}else{
									// No context
									$curline = $start;
								}
								
							}else{
								$this->errors['fatal'][] = "Malformed page range `{$this->pd[$i]}` at patch line `$i`";
							}
						}else{
							$this->errors['fatal'][] = "Malformed pages range `{$this->pd[$i]}` at patch line `$i`";
						}
					break;
					case "\n":
					case ' ':
						if(trim($this->file_data[$curfile][++$curline]) != trim(substr($this->pd[$i],1))){
							$this->errors['fatal'][] = "`{$curfile}` line `$curline` does not match patch contents at patch line `$i`";
							$hunk_ok = false;
						}
					break;
					case '-':
						// Remove this line
						if(trim($this->file_data[$curfile][$curline+1]) == trim(substr($this->pd[$i],1))){
							array_splice($this->file_data[$curfile],$curline+1,$curlines,array_slice($this->file_data[$curfile],$curline+2));
						}else{
							$this->errors['fatal'][] = "`{$curfile}` line `$curline` does not match contents to remove at patch line `$i`";
							$hunk_ok = false;
						}
					break;
					case '+':
						// Add this line
						array_splice($this->file_data[$curfile],++$curline,0,substr($this->pd[$i],1));
					break;
					case '\\':
						// No newline at end of file
						$this->file_data[$curfile][count($this->file_data[$curfile])-1] = preg_replace("#\n$#",'',$this->file_data[$curfile][count($this->file_data[$curfile])-1]);
					break;
					default:
						if(isset($this->fs_operations[$this->pd[$i][0]])){
							
							switch($this->pd[$i][0]){
								case 'c':
								case 'f':
									// Create something, requires write permission to parent directory
									$file = $this->base.'/'.trim(substr($this->pd[$i],1));
									$parent_dir = dirname($file);
									
									if(!is_dir($parent_dir) && !$this->willbe_dir($parent_dir)){
										$this->errors['fatal'][] = "Parent directory `$parent_dir` does not exist at patch line `$i`";
									}elseif(file_exists($file)){
										$this->errors['warning'][] = "Filesystem entity `$file` already exists, skipping creation at patch line `$i`";
										continue;
									}
								break;
								case 'r':
									// delete file or directory
									$file = $this->base.'/'.trim(substr($this->pd[$i],1));
									
									if(!file_exists($file)){
										$this->errors['warning'][] = "Filesystem entity `$file` does not exists, skipping deletion at patch line `$i`";
										continue;
									}
								break;
								case 'b':
									// Write binary to a file, must have write permissions to file
									$data = trim(substr($this->pd[$i],1));
									$data = explode(':',$data);
									$file = $this->base.'/'.$data[0];
									
									if(!is_file($file)){
										$this->errors['fatal'][] = "File `$file` does not exists, binary write failed at patch line `$i`";
									}
								break;
								case 'm':
									$file = $this->base.'/'.trim(substr($this->pd[$i],5));
									
									if(!file_exists($file) && !$this->willbe_dir($file) && !$this->willbe_file($file)){
										$this->errors['fatal'][] = "Filesystem entity `$file` does not exist, CHMOD failed at patch line `$i`";
									}
								break;
							}
							
							$this->fs_operations[$this->pd[$i][0]][] = trim($this->pd[$i]);
							
						}else{
							$this->errors['fatal'][] = "Invalid patch syntax at patch line `$i`";
							break 2;
						}
					break;
				}

			}
			
			if(empty($this->errors['fatal'])){
				
				// Create any empty directories or files that need created
				foreach($this->fs_operations['c'] as $line){
					// Create empty directory
					$this->ftp->mkdir($this->base.'/'.trim(substr($line,1)));
				}
				
				foreach($this->fs_operations['f'] as $line){
					// Create empty file
					$this->ftp->fwrite($this->base.'/'.trim(substr($line,1)),'');
				}
				
				// Delete any directories or files that need deleted
				foreach($this->fs_operations['r'] as $line){
					// Delete directory
					$this->ftp->delete($this->base.'/'.trim(substr($line,1)));
				}
				
				// Write binary
				foreach($this->fs_operations['b'] as $line){
					$data = explode(":",trim(substr($line,1)));
					$this->ftp->fwrite($this->base.'/'.trim($data[0]),pack("H*",$data[1]));
				}
				
				foreach($this->fs_operations['m'] as $line){
					$this->ftp->chmod(substr($line,5),substr($line,1,4));
				}
				
				// Write the diffs
				foreach($this->file_data as $fname=>$fdata){
					$this->ftp->fwrite($fname,implode('',$fdata));
				}
				
			}
			
			return $this->errors;
		}
		
		function willbe_dir($dir){
			return in_array('c'.preg_replace("#^{$this->base}/#",'',$dir),$this->fs_operations['c']);
		}
		
		function willbe_file($file){
			return in_array('f'.preg_replace("#^{$this->base}/#",'',$file),$this->fs_operations['f']);
		}
		
	}
	
	include("./lib_ftp.php");
	$ftp = new lib_ftp('localhost','tchamne1@x7chat.com','zzilb','/patch_test/test_orig');
	
	if($ftp->error != '')
		die($ftp->error);
	
	$patch = new lib_patch('./patch.patch',$ftp);
	print_r($patch->apply_patch());

?>